{****************************************************************************
 * Project : FileSearch library
 * Author  : Alexandre GAMBIER
 * Date    : 24/09/2002
 * Unit    : FileSearch
 * Prefixe :
 * Purpose : Search file & folder
 ****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : File creation
 * Alexandre GAMBIER (26/09/2002) : Add Date Filter
 * Alexandre GAMBIER (27/09/2002) : Add Size Filter
 * Alexandre GAMBIER (14/10/2002) : 1/ Replace OnFileFined event
 *                                     with OnFileFound
 *                                  2/ Add ExcludeFilters
 *                                  3/ NbPathFounded >> NbPathFound
 *                                  4/ NbFilesFounded >> NbFilesFound
 * Alexandre GAMBIER (28/10/2002) : 1/ Add FoundInNbPath in the statistics
 *                                  2/ The LookForDirectory is not override
 *                                     by the LookForAnyFile
 *                                  3/ The Filter property is replace by
 *                                     the FileNames property which allow to
 *                                     select more filters
 *                                  4/ Add the OnAcceptFile event to allow
 *                                     user to decide if the must really
 *                                     be accept                             
 ****************************************************************************}


unit FileSearch;

interface

uses Windows, Classes, Sysutils, Masks;

type
  { Search Options }
  TSearchOptions = record
    IncludeSubfolder    : Boolean;    { Search in subfolders     }
    LookForReadOnlyFile : Boolean;    { Search for readonly file }
    LookForHiddenFile   : Boolean;    { Search for hidden file   }
    LookForSystemFile   : Boolean;    { Search for system file   }
    LookForDirectory    : Boolean;    { Search for folder        }
    LookForArchiveFile  : Boolean;    { Search for archive file  }
    LookForAnyFile      : Boolean;    { Search for any file      }
  end;

  { Type of Date filter access }
  TDateFilterAccessKind = (dfakCreatedFiles, dfakModifiedFiles, dfakOpenedFiles, dfakAnyFiles);

  { Type of date filter }
  TDateFilterKind = (dfkBetween, dfkBefore, dfkAfter, dfkSame);

  { Date Filter }
  TDateFilter = record
    FilterOnDate     : Boolean              ;   { Activate or not the  date filter }
    FilterAccessKind : TDateFilterAccessKind;   { King of access filter            }
    DateFilterKind   : TDateFilterKind      ;   { King of date filter              }
    FirstDate        : TDateTime            ;   { First date                       }
    SecondDate       : TDateTime            ;   { Second date                      }
  end;

  { Type of size filter }
  TSizeFilterKind = (sfkSmallerOrEqualTo, sfkBiggerOrEqualTo);

  { Size Filter }
  TSizeFilter = record
    FilterOnSize   : Boolean        ;   { Activate or not the size filter }
    SizeFilterKind : TSizeFilterKind;   { Kind of filter                  }
    Size           : Integer        ;   { Size to compare with            }
  end;


  { Date Informations }
  TFileTime = record
    CreationTime : SYSTEMTIME;    { The time the file was created }
    AccessTime   : SYSTEMTIME;    { The time the file was opened  }
    WriteTime    : SYSTEMTIME;    { The time the file was written }
  end;

  { File Informations }
  TFileInformations = record
    Name       : AnsiString   ;   { File name       }
    Path       : AnsiString   ;   { PathName        }
    Attributes : Integer  ;   { File attirbutes }
    Size       : Integer  ;   { Size            }
    Time       : TFileTime;   { Times access    }
  end;

  { Statistics }
  TStatistics = record
    NbFilesFound  : LongWord;     { Number of files found matching with serach criteria            }
    FoundInNbPath : LongWord;     { Count the in how many different path the files have been found }
    NbPathFound   : LongWord;     { Number of folders found matching with search criteria          }
    TimeNeeded    : LongWord;     { Time needed by the search operation                            }
  end;

  { Event : OnFileFound > called when a file or a folder is found }
  TOnFileFound   = procedure (FileFound : TFileInformations) of object;

  { Even : OnChangeFolder > called when the search operation change the path }
  TOnChangeFolder = procedure (NewPath : String) of object;

  { Event : OnStatistics > called when search operation is finished }
  TOnStatistics = procedure (Stats : TStatistics) of object;

  { Event : OnAcceptFile > called when a file is about to be accept and the user can say yes or no }
  TOnAcceptFile = function (FileFound : TFileInformations) : Boolean of object;

  { TFileSearch class }
  TFileSearch = class(TObject)
    private

    protected
      { FileInformation functions }
      function GetFileInformations(Path : String; SearchInfos : TSearchRec) : TFileInformations;

      { Filter on attribute }
      function GetAttributeFilter : Integer;
      function IsAttributesOk(AttrToCheck : Integer) : Boolean;

      { Filter on date }
      function IsDateOk(FileInfos : TFileInformations) : Boolean;

      { Filter on Size }
      function IsSizeOk(FileInfos : TFileInformations; SizeFilter : TSizeFilter) : Boolean;

      { Include filter }
      function IsFileMatching(FileName : String) : Boolean;
      function IsAllFileMarkerPresents : Boolean;

      { Exclude Filter }
      function ExcludeFile(FileInfos : TFileInformations; Exclude : TStringList) : Boolean;

    public
      { Search criteria }
      SearchOptions : TSearchOptions;
      DateOptions   : TDateFilter   ;
      SizeOptions   : TSizeFilter   ;
      FileNames     : TStringList   ;
      RootPath      : String        ;
      { Exclude filter }
      ExcludeFilters : TStringList;
      { Events }
      OnFileFound    : TOnFileFound   ;
      OnChangeFolder : TOnChangeFolder;
      OnStatistics   : TOnStatistics  ;
      OnAcceptFile   : TOnAcceptFile  ;

      { Constructor & Destructor }
      constructor Create;
      destructor Destroy; override;

      { start search operation }
      function Search : Boolean;

  end;

implementation

{*****************************************************************************
 * Procedure : TFileSearch.Create
 * Purpose   : Constructor
 * Arguments : NONE
 * Result    : NONE
 *****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : Creation
 *****************************************************************************}
constructor TFileSearch.Create;
begin
  inherited Create;

  { Initialize search options }
  SearchOptions.IncludeSubfolder    := False;
  SearchOptions.LookForReadOnlyFile := True ;
  SearchOptions.LookForHiddenFile   := True ;
  SearchOptions.LookForSystemFile   := True ;
  SearchOptions.LookForDirectory    := True ;
  SearchOptions.LookForArchiveFile  := True ;
  SearchOptions.LookForAnyFile      := True ;

  { Initialize date filter }
  DateOptions.FilterOnDate     := False       ;
  DateOptions.FilterAccessKind := dfakAnyFiles;
  DateOptions.DateFilterKind   := dfkBefore   ;
  DateOptions.FirstDate        := Date        ;
  DateOptions.SecondDate       := Date        ;

  { Initialize size filter }
  SizeOptions.FilterOnSize   := False              ;
  SizeOptions.SizeFilterKind := sfkSmallerOrEqualTo;
  SizeOptions.Size           := 0                  ;

  { Initialize exclude filter }
  ExcludeFilters := TStringList.Create;

  { Filter & RootPath }
  FileNames := TStringList.Create;
  FileNames.Add('*.*');
  RootPath := '.\' ;

  { Events }
  OnFileFound  := nil;
  OnChangeFolder := nil;
  OnStatistics   := nil;
end;

{*****************************************************************************
 * Procedure : TFileSearch.Destroy
 * Purpose   : Destructor
 * Arguments : NONE
 * Result    : NONE
 *****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : Creation
 *****************************************************************************}
destructor TFileSearch.Destroy;
begin
  { free exclude filter }
  FileNames.Free;
  ExcludeFilters.Free;
  
  inherited Destroy;
end;

{*****************************************************************************
 * Procedure : TFileSearch.GetAttributeFilter
 * Purpose   : Initialize the attribute filter
 * Arguments : NONE
 * Result    : Return the attribute filter
 *****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : Creation
 *****************************************************************************}
function TFileSearch.GetAttributeFilter : Integer;
begin
  Result := 0;
  if SearchOptions.LookForReadOnlyFile=True then Result := Result or faReadOnly ;
  if SearchOptions.LookForHiddenFile  =True then Result := Result or faHidden   ;
  if SearchOptions.LookForSystemFile  =True then Result := Result or faSysFile  ;
  if SearchOptions.LookForDirectory   =True then Result := Result or faDirectory;
  if SearchOptions.LookForArchiveFile =True then Result := Result or faArchive  ;
end;

{*****************************************************************************
 * Procedure : TFileSearch.GetFileInformations
 * Purpose   : Convert file information into TFileInformations
 * Arguments : Path        > pathname of the file
 *             SearchInfos > informations given by FindFirst & FindNext
 * Result    : Return TFileInformation about the the file
 *****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : Creation
 *****************************************************************************}
function TFileSearch.GetFileInformations(Path : String; SearchInfos : TSearchRec) : TFileInformations;
var
  TmpSysTime  : SYSTEMTIME;
  TmpFileTime : FILETIME;
begin
  Result.Name       := SearchInfos.Name;
  Result.Path       := Path            ;
  Result.Attributes := SearchInfos.Attr;
  Result.Size       := SearchInfos.Size;
  { Convert time access }
  FileTimeToLocalFileTime(SearchInfos.FindData.ftCreationTime, TmpFileTime);
  FileTimeToSystemTime(TmpFileTime, TmpSysTime);
  Result.Time.CreationTime := TmpSysTime;

  FileTimeToLocalFileTime(SearchInfos.FindData.ftLastAccessTime, TmpFileTime);
  FileTimeToSystemTime(TmpFileTime, TmpSysTime);
  Result.Time.AccessTime := TmpSysTime;

  FileTimeToLocalFileTime(SearchInfos.FindData.ftLastWriteTime, TmpFileTime);
  FileTimeToSystemTime(TmpFileTime, TmpSysTime);
  Result.Time.WriteTime := TmpSysTime;
end;

{*****************************************************************************
 * Procedure : TFileSearch.IsAttributesOk
 * Purpose   : Check if attributes are equals
 * Arguments : AttrToCheck   > Attribute 1
 * Result    : True if ok
 *****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : Creation
 *****************************************************************************}
function TFileSearch.IsAttributesOk(AttrToCheck : Integer) : Boolean;
var
  AttrFilter : Integer;
begin
  Result := False;

  AttrFilter := GetAttributeFilter;

  if ((AttrToCheck and faReadOnly ) and (AttrFilter and faReadOnly ))<>0 then Result := True;
  if ((AttrToCheck and faHidden   ) and (AttrFilter and faHidden   ))<>0 then Result := True;
  if ((AttrToCheck and faSysFile  ) and (AttrFilter and faSysFile  ))<>0 then Result := True;
  if ((AttrToCheck and faDirectory) and (AttrFilter and faDirectory))<>0 then Result := True;
  if ((AttrToCheck and faArchive  ) and (AttrFilter and faArchive  ))<>0 then Result := True;
end;

{*****************************************************************************
 * Procedure : TFileSearch.IsDateOk
 * Purpose   : Check (if date filter is activate) if the file match with
 *             the filter
 * Arguments : FileInfos  > File Informations
 * Result    : Return True if the file match
 *****************************************************************************
 * Alexandre GAMBIER (26/09/2002) : Creation
 *****************************************************************************}
function TFileSearch.IsDateOk(FileInfos : TFileInformations) : Boolean;
var
  CreationTime : TDateTime;
  AccessTime   : TDateTime;
  WriteTime    : TDateTime;
  CreationOk   : Boolean  ;
  AccessOk     : Boolean  ;
  WriteOk      : Boolean  ;
  DateFilterOk : Boolean  ;
  FileInfosRef : TFileInformations;
  SysTime      : SYSTEMTIME;

begin

  { Check if filter on Date is activate }
  if DateOptions.FilterOnDate=False then DateFilterOk := True else
  begin
    { Initialize variable}
    DateFilterOk := False;
    CreationOk   := False;
    AccessOk     := False;
    WriteOk      := False;
    FileInfosRef := FileInfos;

    { Set time to 00:00:00 }
    { First Date}
    DateTimeToSystemTime(DateOptions.FirstDate, SysTime);
    SysTime.wHour         := 0;
    SysTime.wMinute       := 0;
    SysTime.wSecond       := 0;
    SysTime.wMilliseconds := 0;
    DateOptions.FirstDate := SystemTimeToDateTime(SysTime);
    { Second Date}
    DateTimeToSystemTime(DateOptions.SecondDate, SysTime);
    SysTime.wHour         := 0;
    SysTime.wMinute       := 0;
    SysTime.wSecond       := 0;
    SysTime.wMilliseconds := 0;
    DateOptions.SecondDate := SystemTimeToDateTime(SysTime);
    { Creation Date }
    FileInfosRef.Time.CreationTime.wHour         := 0;
    FileInfosRef.Time.CreationTime.wMinute       := 0;
    FileInfosRef.Time.CreationTime.wSecond       := 0;
    FileInfosRef.Time.CreationTime.wMilliseconds := 0;
    { Access Date }
    FileInfosRef.Time.AccessTime.wHour         := 0;
    FileInfosRef.Time.AccessTime.wMinute       := 0;
    FileInfosRef.Time.AccessTime.wSecond       := 0;
    FileInfosRef.Time.AccessTime.wMilliseconds := 0;
    { Write Date }
    FileInfosRef.Time.WriteTime.wHour         := 0;
    FileInfosRef.Time.WriteTime.wMinute       := 0;
    FileInfosRef.Time.WriteTime.wSecond       := 0;
    FileInfosRef.Time.WriteTime.wMilliseconds := 0;

    { Convert Time  format }
    CreationTime := SystemTimeToDateTime(FileInfosRef.Time.CreationTime);
    AccessTime   := SystemTimeToDateTime(FileInfosRef.Time.AccessTime  );
    WriteTime    := SystemTimeToDateTime(FileInfosRef.Time.WriteTime   );

    { switch DateFilter }
    case DateOptions.DateFilterKind of
      { Check between 2 date}
      dfkBetween:
      begin
        if (CreationTime>=DateOptions.FirstDate) and (CreationTime<=DateOptions.SecondDate) then CreationOk := True;
        if (AccessTime  >=DateOptions.FirstDate) and (AccessTime  <=DateOptions.SecondDate) then AccessOk   := True;
        if (WriteTime   >=DateOptions.FirstDate) and (WriteTime   <=DateOptions.SecondDate) then WriteOk    := True;
      end;
      { Check before }
      dfkBefore:
      begin
        if CreationTime<DateOptions.FirstDate then CreationOk := True;
        if AccessTime  <DateOptions.FirstDate then AccessOk   := True;
        if WriteTime   <DateOptions.FirstDate then WriteOk    := True;
      end;
      { Check after }
      dfkAfter  :
      begin
        if CreationTime>DateOptions.FirstDate then CreationOk := True;
        if AccessTime  >DateOptions.FirstDate then AccessOk   := True;
        if WriteTime   >DateOptions.FirstDate then WriteOk    := True;
      end;
      { Check the same }
      dfkSame:
      begin
        if CreationTime=DateOptions.FirstDate then CreationOk := True;
        if AccessTime  =DateOptions.FirstDate then AccessOk   := True;
        if WriteTime   =DateOptions.FirstDate then WriteOk    := True;
      end;
    end;

    { switch filter access king }
    case DateOptions.FilterAccessKind of
      { On Creation Time }
      dfakCreatedFiles : if CreationOk=True then DateFilterOk := True;
      { On Write Time }
      dfakModifiedFiles: if WriteOk=True then DateFilterOk := True;
      { On Access Time }
      dfakOpenedFiles  : if AccessOk=True then DateFilterOk := True;
      { On any file }
      dfakAnyFiles     : if (CreationOk=True) or (AccessOk=True) or (WriteOk=True) then DateFilterOk := True;
    end;
  end;

  Result := DateFilterOk;

end;

{*****************************************************************************
 * Procedure : TFileSearch.IsSizeOk
 * Purpose   : Check (if size filter is activate) if the file match with
 *             the filter
 * Arguments : FileInfos  > File informations
 *             SizeFilter > Size Filter options
 * Result    : Return true if the file match
 *****************************************************************************
 * Alexandre GAMBIER (27/09/2002) : Creation
 *****************************************************************************}
function TFileSearch.IsSizeOk(FileInfos : TFileInformations; SizeFilter : TSizeFilter) : Boolean;
var
  SizeFilterOk : Boolean;
begin
  { Check if filter on Size is activate }
  if SizeFilter.FilterOnSize=False then SizeFilterOk := True else
  begin
    SizeFilterOk := False;

    case SizeFilter.SizeFilterKind of
      { Smaller or equal to }
      sfkSmallerOrEqualTo: if FileInfos.Size<=SizeFilter.Size then SizeFilterOk := True;
      { Bigger or equal to }
      sfkBiggerOrEqualTo : if FileInfos.Size>=SizeFilter.Size then SizeFilterOk := True;
    end;
  end;

  Result := SizeFilterOk;
end;

{*****************************************************************************
 * Procedure : TFileSearch.IsFileMatching
 * Purpose   : Check if the filename matches whith the filenames filter
 * Arguments : FileName       > Filename to match
 * Result    : Returns True if matching, False if not
 *****************************************************************************
 * Alexandre GAMBIER (28/10/2002) : Creation
 *****************************************************************************}
function TFileSearch.IsFileMatching(FileName : String) : Boolean;
var
  VerifMask    : TMask;
  FileMatching : Boolean;
  Pos          : Integer;
begin
  FileMatching := False;
  Pos          := 0;

  while (FileMatching=False) and (Pos<FileNames.Count) do
  begin
    { Initialize }
    VerifMask := TMask.Create(FileNames.Strings[Pos]);
    { Check}
    FileMatching := VerifMask.Matches(FileName);
    { Free }
    VerifMask.Free;
    { Next FileName }
    Inc(Pos);
  end;

  Result := FileMatching;
end;

{*****************************************************************************
 * Procedure : TFileSearch.IsAllFileMarkerPresents
 * Purpose   : Check if the marker *.* is present in the Filenames
 * Arguments : FileNamesMatch > filenames to check
 * Result    :
 *****************************************************************************
 * Alexandre GAMBIER (28/10/2002) : Creation
 *****************************************************************************}
function TFileSearch.IsAllFileMarkerPresents : Boolean;
var
  Index : Integer;
begin
  Result := FileNames.Find('*.*', Index);
end;

{*****************************************************************************
 * Procedure : ExcludeFile
 * Purpose   : Check if the file must be exclude
 * Arguments : FileInfos  > File informations
 *             Exclude    > exclude filter
 * Result    : True if the file must be exclude
 *****************************************************************************
 * Alexandre GAMBIER (14/10/2002) : Creation
 *****************************************************************************}
function TFileSearch.ExcludeFile(FileInfos : TFileInformations; Exclude : TStringList) : Boolean;
var
  VerifMask   : TMask;
  PosFilter   : Integer;
  MustExclude : Boolean;
begin
  { Init }
  MustExclude := False;
  PosFilter   := 0;

  { Check each exclude filter }
  while (MustExclude=False) and (PosFilter<Exclude.Count) do
  begin
    { Initialize }
    VerifMask := TMask.Create(Exclude.Strings[PosFilter]);
    { Check}
    MustExclude := VerifMask.Matches(FileInfos.Name);
    { Free }
    VerifMask.Free;
    { Next filter }
    Inc(PosFilter);
  end;

  Result := MustExclude;
end;

{*****************************************************************************
 * Procedure : TFileSearch.Search
 * Purpose   : Search files & folder using Search criteria
 * Arguments : NONE
 * Result    : True si ok
 *****************************************************************************
 * Alexandre GAMBIER (24/09/2002) : Creation
 * Alexandre GAMBIER (14/10/2002) : Add exclude filters
 * Alexandre GAMBIER (28/10/2002) : 1/ Add FoundInNbPath in the statistics
 *****************************************************************************}
function TFileSearch.Search : Boolean;
var
  bAcceptFile : Boolean;
  bCont       : Boolean;
  bNewPath    : Boolean;
  bStop       : Boolean;
  bFirstPass  : Boolean;
  ListePath   : TStringList;
  sPathName   : String;
  sFiltreOk   : String;
  stFindData  : TSearchRec;
  FileInfos   : TFileInformations;
  Stats       : TStatistics;
  StartTime   : LongWord;
  IncludeFile : Boolean;

Begin
  Result := true;

  { Initialize variables }
  ListePath    := TStringList.Create;
  bCont        := True;
  bFirstPass   := True;
  sPathName    := RootPath;
  { Initialize statistics }
  Stats.NbFilesFound   := 0;
  Stats.FoundInNbPath  := 0;
  Stats.NbPathFound    := 0;
  Stats.TimeNeeded     := 0;

  StartTime := GetTickCount();

  { Search... }
  while bCont=True do
  begin
    bNewPath := True;
    { Search in folder found previous pass }
    if bFirstPass=False then
    begin
      if ListePath.Count=0 then bCont := False
      else
      begin
        { Get PathName }
        sPathName := ListePath.Strings[0];
        ListePath.Delete(0);
      end;
    end;

    if bCont=True then
    begin
      { Alert about folder change }
      if assigned(OnChangeFolder)=True then OnChangeFolder(sPathName);

      { Initialize filter mask }
      if sPathName[Length(sPathName)]<>'\' then sPathName := sPathName + '\';
      sFiltreOk := sPathName + '*.*';

      { Start search operation for current folder }
      if FindFirst(sFiltreOk, faAnyFile, stFindData)=0 then
      begin
        bStop := False;
        { Look for each files & folder }
        while bStop=False and bCont=True do
        begin
          if (stFindData.Name<>'.') and (stFindData.Name<>'..') then
          begin
            { Save folder name for next pass - if options is set }
            if ((stFindData.Attr and faDirectory)<>0) and (SearchOptions.IncludeSubfolder=True) then ListePath.Add(sPathName+stFindData.Name);

            { Initialize file informations }
            FileInfos := GetFileInformations(sPathName, stFindData);

            { Include File ? }
            IncludeFile := False;
            if IsAttributesOk(stFindData.Attr)=True then IncludeFile := True;
            if (SearchOptions.LookForAnyFile=True) and ((stFindData.Attr and faDirectory)=0) then IncludeFile := True;
            { Verify Date filter }
            if IsDateOk(FileInfos)=False then IncludeFile := False;
            { Verify Size filter }
            if IsSizeOk(FileInfos, SizeOptions)=False then IncludeFile := False;
            { Exclude File ? }
            if ExcludeFile(FileInfos, ExcludeFilters)=True then IncludeFile := False;
            { Don't include file }
            if (stFindData.Attr and faDirectory)<>0 then
            begin
              { folder Check }
              if (IsFileMatching(stFindData.Name)=False) and (IsAllFileMarkerPresents=False) then IncludeFile := False;
            end
            else if IsFileMatching(stFindData.Name)=False then IncludeFile := False; { FileCheck}

            if IncludeFile=True then
            begin
              { User says if the file is accepted }
              bAcceptFile := True;
              if Assigned(OnAcceptFile)=True then bAcceptFile := OnAcceptFile(FileInfos);

              if bAcceptFile=True then
              begin
                if (stFindData.Attr and faDirectory)<>0 then
                begin { folder }
                  Inc(Stats.NbPathFound);
                  { Event }
                  if assigned(OnFileFound)=True then OnFileFound(FileInfos);
                end
                else
                begin { File }
                  { Stats}
                  Inc(Stats.NbFilesFound);
                  if bNewPath=True then
                  begin
                    { If in new folder Increment the number of different folder }
                    bNewPath := False;
                    Inc(Stats.FoundInNbPath);
                  end;
                  { Event }
                  if assigned(OnFileFound)=True then OnFileFound(FileInfos);
                end;
              end;

            end;
          end;

          { next file or folder }
          if FindNext(stFindData)<>0 then bStop := True;
        end;

        { close the current search operation }
        FindClose(stFindData);
      end
      else
      begin
       bCont  := False;
       Result := False;
      end;

      bFirstPass := False;
    end;
  end;

  Stats.TimeNeeded  := GetTickCount()-StartTime;
  { send statistics }
  if assigned(OnStatistics)=True then OnStatistics(Stats);

  { free memory }
  ListePath.Free;
end;

End.




